﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using XMLSchemaExtractor.Generator.XSDEntities;

namespace XMLSchemaExtractor.Generator
{
    class XSD : IGenerator
    {
        private XmlDocument xmlDoc;
        private List<string> schema;
        private List<string> message;

        #region xsd tags
        private static string VERSION = "<?xml version=\"1.0\"?>";
        private static string NAMESPACE = "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\">";
        private static string NAMESPACE_CLOSING = "</xs:schema>";
        private static string ELEM = "{0}<xs:element name=\"{1}\">";
        private static string ELEM_EMPTY = "{0}<xs:element name=\"{1}\"/>";
        private static string ELEM_TYPE = "{0}<xs:element name=\"{1}\" type=\"{2}\"/>";
        private static string ELEM_DEFAULT = "{0}<xs:element name=\"{1}\" type=\"{2}\" default=\"{3}\"/>";
        private static string ELEM_FIXED = "{0}<xs:element name=\"{1}\" type=\"{2}\" fixed=\"{3}\"/>";
        private static string ELEM_OCCURRENCE = "{0}<xs:element name=\"{1}\" maxOccurs=\"{2}\">";
        private static string ELEM_EMPTY_OCCURRENCE = "{0}<xs:element name=\"{1}\" maxOccurs=\"{2}\"/>";
        private static string ELEM_TYPE_OCCURRENCE = "{0}<xs:element name=\"{1}\" maxOccurs=\"{2}\" type=\"{3}\"/>";
        private static string ELEM_CLOSING = "{0}</xs:element>";
        private static string SIMPLE_CONTENT = "{0}<xs:simpleContent>";
        private static string SIMPLE_CONTENT_CLOSING = "{0}</xs:simpleContent>";
        private static string COMPLEX_CONTENT = "{0}<xs:complexContent>";
        private static string COMPLEX_CONTENT_CLOSING = "{0}</xs:complexContent>";
        private static string ATTR = "{0}<xs:attribute name=\"{1}\" type=\"{2}\"/>";
        private static string ATTR_DEFAULT = "{0}<xs:attribute name=\"{1}\" type=\"{2}\" default=\"{3}\"/>";
        private static string ATTR_FIXED = "{0}<xs:attribute name=\"{1}\" type=\"{2}\" fixed=\"{3}\"/>";
        private static string ATTR_REQUIRED = "{0}<xs:attribute name=\"{1}\" type=\"{2}\" use=\"required\"/>";
        private static string ATTR_CLOSING = "{0}</xs:attribute>";
        private static string EXTENSION = "{0}<xs:extension base=\"{1}\">";
        private static string EXTENSION_CLOSING = "{0}</xs:extension>";
        private static string RESTRICTION = "{0}<xs:restriction base=\"{1}\">";
        private static string RESTRICTION_CLOSING = "{0}</xs:restriction>";
        private static string COMPLEX_TYPE = "{0}<xs:complexType>";
        private static string COMPLEX_TYPE_NAME = "{0}<xs:complexType name=\"{1}\">";
        private static string COMPLEX_TYPE_MIXED = "{0}<xs:complexType mixed=\"{1}\">";
        private static string COMPLEX_TYPE_CLOSING = "{0}</xs:complexType>";
        private static string SEQUENCE = "{0}<xs:sequence>";
        private static string SEQUENCE_CLOSING = "{0}</xs:sequence>";
        #endregion

        #region types
        private string[] stringTypes = { "xs:IDREF", "xs:string", "xs:normalizedString", "xs:token" };
        private string[] dateTypes = { "xs:date", "xs:time", "xs:dateTime", "xs:duration" };
        private string[] numericTypes = { "xs:decimal", "xs:integer", "xs:negativeInteger" };
        private string[] miscTypes = { "xs:boolean", "xs:double", "xs:float" };
        enum StringEnum { idref = 0, stringType, normStr, token }
        enum DateEnum { date = 0, time, dateTime, duration }
        enum NumericEnum { decimalType = 0, integer, negInt }
        enum MiscEnum { boolean = 0, doubleType, floatType }
        #endregion

        public XSD(XmlDocument xmlDoc)
        {
            this.xmlDoc = xmlDoc;
            this.schema = new List<string>();
            this.message = new List<string>();
        }

        public string[] GetSchema()
        {
            int level = 1;
            XsdElement ele = new XsdElement();
            XmlNode root = xmlDoc.DocumentElement;
            XmlNodeList children = root.ChildNodes;
            ele.Name = root.Name;
            schema.Add(VERSION);
            schema.Add(NAMESPACE);
            GenerateChildren(ele, children);
            ele.CleanUp();
            GenerateSchema(ele, level);
            schema.Add(string.Format(NAMESPACE_CLOSING));
            return schema.ToArray();
        }

        public string[] GetMessage()
        {
            return message.ToArray();
        }

        private void GenerateSchema(XsdElement root, int level)
        {
            if (root.Type.Equals("complexType"))
            {
                GenerateComplexTypeSchema(root, level);
            }
            else if (root.Type.Equals("complexContent"))
            {
                GenerateComplexContentSchema(root, level);
            }
            else if (root.Type.Equals("simpleContent"))
            {
                GenerateSimpleContentSchema(root, level);
            }
            else if (root.Type.Equals("emptyType"))
            {
                GenerateElementSchema(root, level);
            }
            else
            {
                GenerateElementSchema(root, level);
            }
        }

        private void GenerateElementSchema(XsdElement root, int level)
        {
            if (root.Occurrence < 2)
            {
                switch (root.Type)
                {
                    case "emptyType":
                        schema.Add(string.Format(ELEM_EMPTY, GenerateSpace(level), root.Name));
                        break;
                    case "complexType":
                    case "complexContent":
                    case "simpleContent":
                        schema.Add(string.Format(ELEM, GenerateSpace(level), root.Name));
                        break;
                    default:
                        schema.Add(string.Format(ELEM_TYPE, GenerateSpace(level), root.Name, root.Type));
                        break;
                }
            }
            else
            {
                switch (root.Type)
                {
                    case "emptyType":
                        schema.Add(string.Format(ELEM_EMPTY_OCCURRENCE, GenerateSpace(level), root.Name, "unbounded"));
                        break;
                    case "complexType":
                    case "complexContent":
                    case "simpleContent":
                        schema.Add(string.Format(ELEM_OCCURRENCE, GenerateSpace(level), root.Name, "unbounded"));
                        break;
                    default:
                        schema.Add(string.Format(ELEM_TYPE_OCCURRENCE, GenerateSpace(level), root.Name, "unbounded", root.Type));
                        break;
                }
            }
        }

        private void GenerateSimpleContentSchema(XsdElement root, int level)
        {
            GenerateElementSchema(root, level);
            schema.Add(string.Format(COMPLEX_TYPE, GenerateSpace(level + 1)));
            schema.Add(string.Format(SIMPLE_CONTENT, GenerateSpace(level + 2)));
            schema.Add(string.Format(EXTENSION, GenerateSpace(level + 3), root.BaseType));
            GenerateSchemaAttr(root.Attributes, level + 4);
            schema.Add(string.Format(EXTENSION_CLOSING, GenerateSpace(level + 3)));
            schema.Add(string.Format(SIMPLE_CONTENT_CLOSING, GenerateSpace(level + 2)));
            schema.Add(string.Format(COMPLEX_TYPE_CLOSING, GenerateSpace(level + 1)));
            schema.Add(string.Format(ELEM_CLOSING, GenerateSpace(level)));
        }

        private void GenerateComplexContentSchema(XsdElement root, int level)
        {
            GenerateElementSchema(root, level);
            schema.Add(string.Format(COMPLEX_TYPE, GenerateSpace(level + 1)));
            GenerateSchemaAttr(root.Attributes, level + 2);
            schema.Add(string.Format(COMPLEX_TYPE_CLOSING, GenerateSpace(level + 1)));
            schema.Add(string.Format(ELEM_CLOSING, GenerateSpace(level)));
        }

        private void GenerateComplexTypeSchema(XsdElement root, int level)
        {
            GenerateElementSchema(root, level);
            if (root.IsMixed)
            {
                schema.Add(string.Format(COMPLEX_TYPE_MIXED, GenerateSpace(level + 1), "true"));
            }
            else
            {
                schema.Add(string.Format(COMPLEX_TYPE, GenerateSpace(level + 1)));
            }
            if (root.Children.Count > 0)
            {
                schema.Add(string.Format(SEQUENCE, GenerateSpace(level + 2)));
                foreach (XsdElement child in root.Children)
                {
                    GenerateSchema(child, level + 3);
                }
                schema.Add(string.Format(SEQUENCE_CLOSING, GenerateSpace(level + 2)));
            }
            if (root.Attributes.Count > 0)
            {
                GenerateSchemaAttr(root.Attributes, level + 2);
            }
            schema.Add(string.Format(COMPLEX_TYPE_CLOSING, GenerateSpace(level + 1)));
            schema.Add(string.Format(ELEM_CLOSING, GenerateSpace(level)));
        }

        private void GenerateSchemaAttr(List<XsdAttribute> attributes, int level)
        {
            foreach (XsdAttribute attr in attributes)
            {
                if (attr.IsRequired)
                {
                    schema.Add(string.Format(ATTR_REQUIRED, GenerateSpace(level), attr.Name, attr.Type));
                }
                else
                {
                    schema.Add(string.Format(ATTR, GenerateSpace(level), attr.Name, attr.Type));
                }
            }
        }

        private void GenerateChildren(XsdElement parent, XmlNodeList xmlNodeList)
        {
            foreach (XmlNode node in xmlNodeList)
            {
                if (node.NodeType.Equals(XmlNodeType.XmlDeclaration))
                {
                    continue;
                }
                if (node.NodeType.Equals(XmlNodeType.DocumentType))
                {
                    continue;
                }
                if (node.NodeType.Equals(XmlNodeType.SignificantWhitespace))
                {
                    continue;
                }
                if (node.NodeType.Equals(XmlNodeType.Text))
                {
                    parent.Type = GetType(node.Value);
                    parent.BaseType = parent.Type;
                    parent.Value = node.Value;
                    continue;
                }
                if (node.NodeType.Equals(XmlNodeType.Element))
                {
                    XsdElement ele = parent.AddChild(node.Name);
                    ele.Value = node.Value;
                    if (node.HasChildNodes)
                    {
                        GenerateChildren(ele, node.ChildNodes);
                    }
                    if (node.Attributes != null)
                    {
                        GenerateAttributes(ele, node.Attributes);
                    }
                }
                else
                {
                    continue;
                }
            }
        }

        private void GenerateAttributes(XsdElement ele, XmlAttributeCollection attributes)
        {
            foreach (XmlAttribute attr in attributes)
            {
                XsdAttribute a = ele.AddAttribute(attr.Name);
                a.Type = GetType(attr.Value);
                a.Value = attr.Value;
            }
        }

        private string GenerateSpace(int level)
        {
            if (level == 0) return "";
            else
            {
                level = level - 1;
                return ("   " + GenerateSpace(level));
            }
        }

        private string GetType(string value)
        {
            int intres;
            double doubres;
            bool boolres;
            float floatres;
            DateTime datetimeres;
            if (int.TryParse(value, out intres))
            {
                return numericTypes[(int)NumericEnum.integer];
            }
            else if (double.TryParse(value, out doubres))
            {
                return miscTypes[(int)MiscEnum.doubleType];
            }
            else if (bool.TryParse(value, out boolres))
            {
                return miscTypes[(int)MiscEnum.boolean];
            }
            else if (float.TryParse(value, out floatres))
            {
                return miscTypes[(int)MiscEnum.floatType];
            }
            else if (DateTime.TryParse(value, out datetimeres))
            {
                return dateTypes[(int)DateEnum.dateTime];
            }
            else
            {
                return stringTypes[(int)StringEnum.stringType];
            }
        }

    }
}
